package es.upm.gib.mappingAPI2;

import es.upm.gib.api_sparql.FilterExpression;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * This class represents one entry of the mapping
 * Each entry of the model has:
 * - two views: one conceptual, one physical
 * - brief description
 * - some custom data
 * - author of the entry
 * - date of creation
 * - name of last person who modifies the entry
 * - last date of modification
 * - list of external bounds
 * 
 */
public class Entry implements Serializable{

    //=======================================================================
    //ATTRIBUTES
    //=======================================================================

    /**
     * The conceptual view of this entry
     */
    private View conceptualView;

    /**
     * The physical view of this entry
     */
    private View physicalView;

    /**
     * Natural language description of this entry. This String should contain a short sentence describing which
     * fields or data this view represents. For example: "Patients and their names". Its use is optional, with a
     * null value indicating that there is no description for this entry
     */
    private String description;

    /**
     * Custom data of the entry
     */
    private CustomData customData;

    /**
     * The person who created this entry
     */
    private String author;

    /**
     * Date in which this entry was first created
     */
    private Date creationDate;


    /**
     * The person who last modified this entry
     */
    private String lastModifiedBy;


    /**
     * When this entry was modified for the last time
     */
    private Date lastModificationDate;


    /**
     * Vector of this view's external bounds
     */
    private ArrayList<CoordinatesBound> extBounds = new ArrayList<CoordinatesBound>();


    //=======================================================================
    // CONSTRUCTORS
    //=======================================================================

    /**
     * creates a new entry.
     * </p>
     * the parameters are Views objects representing a conceptual view and a physical view. A view is a set of paths.
     * The format of the path is given in the following example:
     * </p>
     * if we want to define a path from ClassA to ClassC like this: ClassA---[Rel1]--->ClassB---[Rel2]--->ClassC
     * the correspondent Vector will be composed by two triples: [Triple1,Triple2] ,   where Triple1 is ["ClassA","Rel1","ClassB"] and Triple2 is ["ClassB","Rel2","ClassC"]
     * <br>
     * so the final correspondent vector is:<br>
     * [["ClassA","Rel1","ClassB"],["ClassB","Rel2","ClassC"]]
     *
     * @param conceptual a view into the conceptual schema
     * @param physical   a view into the physical schema
     */
    public Entry(View conceptual, View physical) {
        this.conceptualView = conceptual;
        this.physicalView = physical;
        this.customData = new CustomData();
        this.extBounds = new ArrayList<CoordinatesBound>();
    }

    /**
     * Constructor
     */
    Entry(){
        this.customData = new CustomData();
        this.extBounds = new ArrayList<CoordinatesBound>();
    }


    //=======================================================================
    // PUBLIC METHODS
    //=======================================================================


    /**
     * This method returns the custom data defined in the entry
     *
     * @return custom data of the entry
     */
    public CustomData getCustomData() {
        return customData;
    }

    /**
     * This method puts the input custom data into the entry
     * 
     * @param customData CustomData to store
     */
    public void setCustomData(CustomData customData) {
        this.customData = customData;
    }

    /**
     * This method retrieves the list of Identifier classes of the physical view of the entry
     * @return physical view Identifier classes of the entry
     */
    public ArrayList<Identifier> getPhysicalIdentifiers(){
        return physicalView.getIdentifiers();
    }

    /**
     * This method retrieves the list of Identifier classes of the conceptual view of the entry
     * @return conceptual view Identifier classes of the entry
     */
    public ArrayList<Identifier> getConceptualIdentifiers(){
        return conceptualView.getIdentifiers();
    }

    /**
     * This method determines if the input Identifier belongs to the physical view of the entry
     * @param identifier input Identifier
     * @return true if the identifier belongs to the physical view, false otherwise
     */
    public boolean isPhysicalIdentifier(Identifier identifier){
        return physicalView.isIdentifier(identifier);
    }

    /**
     * This method determines if the input Identifier belongs to the conceptual view of the entry
     * @param identifier input Identifier
     * @return true if the identifier belongs to the conceptual view, false otherwise
     */
    public boolean isConceptualIdentifier(Identifier identifier){
        return conceptualView.isIdentifier(identifier);
    }

    /**
     * this method returns all the external bounds defined in the entry.
     * An external bound is a pair of pairs of integer. the first pair of integer refers
     * to the conceptual view of the view, the second pair to the physical view of the entry.
     * for each pair, the first integer represent the number of the path it refers and the
     * second integer refers to the nomber of the class in the path.
     * <p/>
     * for example the external bound ((0,2),(1,1)) bounds:
     * the class 2 of the path 0 in the conceptual view
     * with
     * the class 1 of the path 1 in the physical view
     *
     * @return a vector of External Bounds
     */
    public ArrayList<CoordinatesBound> getExternalBounds() {
        return extBounds;
    }

    /**
     * This method set the list of external bounds into the entry
     *
     * @param extBounds List of external bounds
     */
    public void setExternalBounds(ArrayList<CoordinatesBound> extBounds) {
        this.extBounds = extBounds;
    }

    /**
     * This method retrieves the physical view of the entry
     *
     * @return physical view
     */
    public View getPhysicalView() {
        return physicalView;
    }

    /**
     * This method set the physical view into the entry
     *
     * @param physicalView view to store as physical
     */
    public void setPhysicalView(View physicalView) {
        this.physicalView = physicalView;
    }

    /**
     * This method retrieves the conceptual view of the entry
     *
     * @return conceptual view
     */
    public View getConceptualView() {
        return conceptualView;
    }

    /**
     * This method set the conceptual view into the entry
     *
     * @param conceptualView view to store as conceptual
     */
    public void setConceptualView(View conceptualView) {
        this.conceptualView = conceptualView;
    }

    /**
     * This method returns author name of the entry
     *
     * @return author of the entry
     */
    public String getAuthor() {
        return author;
    }

    /**
     * This method sets author name of the entry
     *
     * @param author name of the author
     */
    public void setAuthor(String author) {
        this.author = author;
    }

    /**
     * This method returns the creation date of the entry
     *
     * @return creation date of the entry
     */
    public Date getCreationDate() {
        return creationDate;
    }

    /**
     * This method sets the creation date of the entry
     *
     * @param creationDate creation date of the entry
     */
    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }

    /**
     * The method retrieves the name of the last author who modifies the entry
     *
     * @return name of last author
     */
    public String getLastModifiedBy() {
        return lastModifiedBy;
    }

    /**
     * The method sets the name of the last author who modifies the entry
     *
     * @param lastModifiedBy name of the author
     */
    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    /**
     * This method retrieves the date of the last modification of the entry
     *
     * @return las modification date
     */
    public Date getLastModificationDate() {
        return lastModificationDate;
    }

    /**
     * This method sets the last modification date of the entry
     *
     * @param lastModificationDate last modification date
     */
    public void setLastModificationDate(Date lastModificationDate) {
        this.lastModificationDate = lastModificationDate;
    }

    /**
     * this method defines a natural language description of this entry.
     *
     * @param desc the natural language description of this entry.
     */
    public void setDescription(String desc) {
        description = desc;
    }

    /**
     * this method returns the natural language description of this entry.
     *
     * @return the natural language description of this entry.
     */
    public String getDescription() {
        return description;
    }

    /**
     * sets the external bounds
     *
     * @param bounds a vector of external bounds
     */
    public void setRestriction(ArrayList<CoordinatesBound> bounds) {
        extBounds = bounds;
    }

    /**
     * This method adds a bound to the external bounds of the entry
     *
     * @param bound bound to add
     * */
    public void addExternalBound(CoordinatesBound bound){
        extBounds.add(bound);
    }

    /**
     * compares two Entry objects. The comparation is true if the
     * values of the instance variables are equivalent.
     * <br>
     * conceptually this means that the two Entry have equivalent conceptual and physical views.
     *
     * @param entry the Entry to compare
     * @return if the two objects are equivalent or not
     */

    public boolean equals(Object entry) {

        if (entry instanceof Entry) {

            Entry instancedEntry = (Entry) entry;

            return (conceptualView.equals(instancedEntry.getConceptualView()) && physicalView.equals(instancedEntry.getPhysicalView()));

        }
        return false;


    }

    /**
     * returns an entry as a string representation.
     *
     * @return an entry as a String
     */
    public String toString() {
        String newString;
        newString = "\nDESCRIPTION:" + description;
        newString = newString + "\nEntry External Bounds: " + extBounds;
        // return newString = (newString+ "\n Entry Paths correspondance:"  + toStringConceptual()  +" <==> "+  toStringPhysical() );
        return (newString + "\n Entry views correspondance:" + conceptualView + " <==> " + physicalView);

    }

    /**
     * Generates the SPARQL query for the physical view.
     *
     * @see #generateSPARQLFromPhysical(java.util.ArrayList)
     *
     * @return A String containing the query
     */
    public String generateSPARQLFromPhysical(){
        ArrayList<String> optionals = new ArrayList<String>();
        return generateSPARQLFromPhysical(optionals);
    }

    /**
     * Generates the SPARQL query for the physical view.
     *
     * @param optionals A list of classes that must be optional in the query. The lack of this parameter means that the
     *                  list of optional classes is going to be empty
     * @return A String containing the query
     */
    public String generateSPARQLFromPhysical(ArrayList<String> optionals){
        return generateSPARQL(optionals,false);
    }

    /**
     * Generates the SPARQL query for the conceptual view.
     *
     * @see #generateSPARQLFromConceptual(java.util.ArrayList)
     *
     * @return A String containing the query
     */
    public String generateSPARQLFromConceptual(){
        ArrayList<String> optionals = new ArrayList<String>();
        return generateSPARQLFromConceptual(optionals);
    }

    /**
     * Generates the SPARQL query for the conceptual view.
     *
     * @param optionals A list of classes that must be optional in the query. The lack of this parameter means that the
     *                  list of optional classes is going to be empty
     * @return A String containing the query
     */
    public String generateSPARQLFromConceptual(ArrayList<String> optionals){
        return generateSPARQL(optionals,true);
    }

    private String generateSPARQL(ArrayList<String> optionals, boolean conceptual){
        // go through the paths of the physical view and generate the SPARQL triples from these paths (add them to the SparqlConstructor)
        // the external bounds of the entry establish which physical variables must be retrieved -> go through them, generate the var names and
        // add them to the SparqlConstructor
        // at the same time that the external bounds are reviewed, a table relating physical var name to conceptual position is saved. This table
        // is returned together with the SPARQL query)
        // the internal bounds of the physical entry establish the FILTERS of the query. The are stored in an ArrayList and added to the
        // SparqlConstructor
        // yet the conditions establish additional FILTERS to set to the query
        // the method returns the generated query in string mode, together with the table of external bounds
        // the table of external bounds is a Map<String, String>, where the key is the position in the conceptual view , and the value is
        // the retrieved var name in the generated query. This position is codified like "path@class", for easy parsing.
        // both elements are returned in a SPARQLQueryGenerator.Result object, which contains both elements

        // query constructor
        String prefixes =   "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n" +
                            "PREFIX owl: <http://www.w3.org/2002/07/owl#>\n" +
                            "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n" +
                            "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>";
        StringBuilder select = new StringBuilder();
        StringBuilder triples = new StringBuilder();
        StringBuilder types = new StringBuilder();
        StringBuilder filters = new StringBuilder();

        int pathNum = 0;
        ArrayList<Path> entryPaths;
        ArrayList<CoordinatesBound> internalBounds;
        if(conceptual){
            entryPaths = this.getConceptualView().getPaths();
            internalBounds = this.getConceptualView().getInternalBounds();
        }else{
            entryPaths = this.getPhysicalView().getPaths();
            internalBounds = this.getPhysicalView().getInternalBounds();
        }

        for (Path path : entryPaths) {
            int classNum = 0;
            String origin;
            String destination;
            StringBuilder aux = new StringBuilder();
            for (Triple triple : path.getPath()) {

                origin = generateVariableName(pathNum, classNum);
                destination = generateVariableName(pathNum, classNum + 1);

                if(optionals.contains(triple.getOrigin()) || optionals.contains(triple.getDestination())){
                    generateQueryTriple(aux,"?"+origin,"<"+triple.getRelation()+">","?"+destination);
                    if(!optionals.contains(triple.getDestination()))
                        optionals.add(triple.getDestination());
                }else
                    generateQueryTriple(triples,"?"+origin,"<"+triple.getRelation()+">","?"+destination);

                int i=0;
                String var,type;
                while(i<2){
                    var = generateVariableName(pathNum, classNum + i);
                    type = (i==0)?triple.getOrigin():triple.getDestination();
                    if(!type.contains("http://www.w3.org")){
                        if(!types.toString().contains("?"+var+" a <"+type+">. \r\n")){
                            //types.append("?"+var+" a <"+type+"> .\r\n");
                            generateQueryTriple(types,"?"+var,"a","<"+type+">");
                        }
                    }
                    i++;
                }
                classNum++;
            }
            if(aux.toString().length()>0){
                setFilters(path,pathNum,aux);
                triples.append("OPTIONAL{\r\n");
                triples.append(aux.toString());
                triples.append("}\r\n");
            }else{
                setFilters(path,pathNum,filters);
            }
            pathNum++;
        }

        for (CoordinatesBound coordinatesBound : this.getExternalBounds()) {
            // the first class and path are for the physical view
            int classNum;
            if(!conceptual){
                pathNum = coordinatesBound.getFirstPath();
                classNum = coordinatesBound.getFirstClass();
            }else{
                pathNum = coordinatesBound.getSecondPath();
                classNum = coordinatesBound.getSecondClass();
            }
            String physicalVarName = "?"+generateVariableName(pathNum, classNum)+" ";
            select.append(physicalVarName);
        }

        // go through the internal bounds of the view
        for (CoordinatesBound coordinatesBound : internalBounds) {
            String var1 = generateVariableName(coordinatesBound.getFirstPath(), coordinatesBound.getFirstClass());
            String var2 = generateVariableName(coordinatesBound.getSecondPath(), coordinatesBound.getSecondClass());
            filters.append("FILTER(?");
            filters.append(var1);
            filters.append(" = ?");
            filters.append(var2);
            filters.append(")\r\n");
        }

        return prefixes+"\r\nSELECT "+select.toString()+"\r\nWHERE{\r\n"+types.toString()+triples.toString()+filters.toString()+"}";
    }

    /**
     * Generates variable names like ?v_pathNum_classNum
     *
     * @param pathNum used to create the variable name
     * @param classNum used to create the variable name
     * @return New variable name
     */
    private static String generateVariableName(int pathNum, int classNum) {
        return "v_" + pathNum + "_" + classNum;
    }

    /**
     * Generates a String that later is going o be used in the External Bounds
     *
     * @param path Path number
     * @param class_ Class number
     * @return Codified position
     */
    static String codifyPosition(int path, int class_) {
        return path + "@" + class_;
    }

    static void setFilters(Path path, int pathNum, StringBuilder filters){
        for(int i=0; i<=path.size(); i++){
            Restriction rest = path.getRestrictions(i);
            if(rest!=null){
                FilterExpression.Operator op = null;
                for(FilterExpression.Operator opR : FilterExpression.Operator.values()){
                    if(opR.name().equals(rest.getOp().name())){
                        op = opR;
                        break;
                    }
                }
                //String var1 = generateVariableName(pathNum,i);

                String Origin;
                String Destination;
                if(rest.getRange().name().equals("INTEGER")){
                    Origin = "FILTER(xsd:double(?"+generateVariableName(pathNum,i)+")";
                    Destination = rest.getValue()+")";
                }else if(rest.getRange().name().equals("INSTANCE")){
                    Origin = "FILTER(?"+generateVariableName(pathNum,i);
                    Destination = "<"+rest.getValue()+">)";
                }
                else{
                    Origin = "FILTER(?"+generateVariableName(pathNum,i);
                    Destination = "\""+rest.getValue()+"\")";
                }
                if (op != null) {
                    generateQueryTriple(filters,Origin,op.getStr(),Destination);
                }
            }
        }
    }

    /**
     * Appends in a StringBuilder given as parameter the three parts of a triple.
     *
     * @param tripleBuilder The result is going to be appended in this StringBuilder
     * @param Origin Origin of the triple
     * @param Relation Relation of the triple
     * @param Destination Destination of the triple
     */
    public static void generateQueryTriple(StringBuilder tripleBuilder, String Origin, String Relation, String Destination){
        tripleBuilder.append(Origin);
        tripleBuilder.append(" ");
        tripleBuilder.append(Relation);
        tripleBuilder.append(" ");
        tripleBuilder.append(Destination);
        tripleBuilder.append(". \r\n");
    }


    /**
     * Generates external bounds with new names to be used in the data translation
     *
     * @return Map containing the externalBounds
     */
    public Map<String, String> getQueryExternalBounds(){

        Map<String, String> externalBounds = new HashMap<String, String>();
        int pathNum;
        for (CoordinatesBound coordinatesBound : this.getExternalBounds()) {
            pathNum = coordinatesBound.getFirstPath();
            int classNum = coordinatesBound.getFirstClass();
            String physicalVarName = generateVariableName(pathNum, classNum);
            String conceptualPosition = codifyPosition(coordinatesBound.getSecondPath(), coordinatesBound.getSecondClass());
            externalBounds.put(conceptualPosition, physicalVarName);
        }

        return externalBounds;
    }


}
